﻿// BookClubService.cs
//

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Data;
using System.Linq;
using System.ServiceModel.DomainServices.EntityFramework;
using System.ServiceModel.DomainServices.Hosting;
using System.ServiceModel.DomainServices.Server;
using BookClub.Web.DataModel;
using BookClub.Web.WebServices;

namespace BookClub.Web.Services {

    // This service represents the book club, and allows a user to browse/search books, and
    // make requests, or mark books as liked.
    [EnableClientAccess]
    public class BookClubService : LinqToEntitiesDomainService<BookClubEntities> {

        private User _user;
        private IBookStoreService _bookStore;
        private INotificationService _notifier;

        public BookClubService(User user, IBookStoreService bookStore, INotificationService notifier) {
            _user = user;
            _bookStore = bookStore;
            _notifier = notifier;
        }

        // Get the latest books shared by book club members.
        [Query(ResultLimit = 5)]
        public IQueryable<BookInfo> GetLatestBooks() {
            IQueryable<Book> bookQuery =
                from b in ObjectContext.Books
                orderby b.AddedDate descending
                select b;

            return ProjectBookIntoBookInfo(bookQuery);
        }

        // Search for books using the specified search text.
        // NOTE: Using searchText parameter to allow implementation to evolve to use full-text
        //       search, rather than have client create a LINQ query against Description,
        //       Title, and Author fields of a book directly.
        [Query(ResultLimit = 5)]
        public IQueryable<BookInfo> SearchBooks(string searchText) {
            IQueryable<Book> bookQuery =
                from b in ObjectContext.Books
                where (b.Title.Contains(searchText) ||
                       b.Author.Contains(searchText) ||
                       b.Description.Contains(searchText))
                orderby b.AddedDate descending
                select b;

            if (bookQuery.Count() == 0) {
                // TODO: Track... query methods can be used to collect analytics
                //       as well. In this case, it might be interesting to track
                //       book club member searches that result in no matching books.
            }

            return ProjectBookIntoBookInfo(bookQuery);
        }

        // Uses a stored procedure to get the book of the day, and merge it along with
        // a list of similar books in the book club. Similar books are retrieved using
        // Amazon API.
        [Query(IsComposable = false)]
        [OutputCache(OutputCacheLocation.Any, 3600)]
        public IEnumerable<BookInfo> GetBookOfTheDay() {
            Book bookOfDay = null;

            try {
                using (BookClubEntities db = new BookClubEntities()) {
                    bookOfDay = db.SelectBookOfDay().First();
                }
            }
            catch {
            }

            string[] similarIDs = _bookStore.FindSimilarProducts(bookOfDay.ASIN);

            IQueryable<Book> bookQuery =
                from b in ObjectContext.Books.Where(b => b.BookID == bookOfDay.BookID).
                          Union(ObjectContext.Books.Where(b => similarIDs.Contains(b.ASIN)).Take(2))
                select b;
            return ProjectBookIntoBookInfo(bookQuery);
        }

        // Allows a book club member to mark a book as liked.
        [RequiresAuthentication]
        public void LikeBook(BookInfo book) {
            bool bookExists = ObjectContext.Books.Any(b => b.BookID == book.BookID);
            if (bookExists == false) {
                throw new ValidationException("The specified book does not exist in the book club.");
            }

            bool existingLike = ObjectContext.Likes.Any(l => l.BookID == book.BookID &&
                                                             l.MemberID == _user.MemberID);
            if (existingLike == false) {
                int likeCount = ObjectContext.Likes.Where(l => l.BookID == book.BookID).Count();

                Like newLike = new Like() {
                    BookID = book.BookID,
                    MemberID = _user.MemberID,
                    LikeDate = DateTime.UtcNow.Date
                };
                ObjectContext.Likes.AddObject(newLike);

                book.Likes = likeCount + 1;
            }
        }

        // Projects a Book object and related objects into a BookInfo object
        private IQueryable<BookInfo> ProjectBookIntoBookInfo(IQueryable<Book> bookQuery) {
            return from b in bookQuery
                   select new BookInfo {
                       BookID = b.BookID,
                       ASIN = b.ASIN,
                       Author = b.Author,
                       Title = b.Title,
                       Description = b.Description,
                       PublishDate = b.PublishDate,
                       CategoryID = b.CategoryID,
                       AddedDate = b.AddedDate,
                       SharedBy = b.Member.MemberName,
                       CheckedOut = b.Checkouts.Any(c => c.Returned == false),
                       Requests = b.Requests.Where(r => r.CheckoutID == 0).Count(),
                       Likes = b.Likes.Count()
                   };
        }

        // Allows a book club member to request a book.
        [RequiresAuthentication]
        public void RequestBook(BookInfo book) {
            bool existingCheckout =
                ObjectContext.Checkouts.Any(c => c.BookID == book.BookID &&
                                            c.MemberID == _user.MemberID &&
                                            c.Returned == false);
            if (existingCheckout) {
                throw new ValidationException("This requested book has already been checked out to you.");
            }

            bool existingRequest =
                ObjectContext.Requests.Any(r => r.BookID == book.BookID &&
                                           r.MemberID == _user.MemberID &&
                                           r.CheckoutID != 0);
            if (existingRequest) {
                throw new ValidationException("You have already requested this book.");
            }

            Request newRequest = new Request() {
                BookID = book.BookID,
                MemberID = _user.MemberID,
                RequestDate = DateTime.UtcNow.Date
            };

            ObjectContext.Requests.AddObject(newRequest);
        }

        protected override bool ValidateChangeSet() {
            // Changeset-level validation to enforce that the client is only allowing
            // one request/one like at a time.
            // This is because the application wants to prevent the possibility of multiple
            // requests/likes for the same book in the same changeset.

            if (ChangeSet.ChangeSetEntries.Count != 1) {
                return false;
            }

            return base.ValidateChangeSet();
        }
    }
}
